package org.wicket.jbpm.engine.core;

import java.io.InputStream;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.Map.Entry;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.LockMode;
import org.hibernate.Session;
import org.jbpm.JbpmContext;
import org.jbpm.JbpmException;
import org.jbpm.db.GraphSession;
import org.jbpm.db.TaskMgmtSession;
import org.jbpm.graph.def.ProcessDefinition;
import org.jbpm.graph.exe.ProcessInstance;
import org.jbpm.graph.exe.Token;
import org.jbpm.taskmgmt.exe.TaskInstance;
import org.springmodules.workflow.jbpm31.JbpmCallback;
import org.springmodules.workflow.jbpm31.JbpmTemplate;
import org.wicket.jbpm.engine.exception.WicketJbpmException;
import org.wicket.jbpm.engine.spring.JbpmProcessEngine;
import org.wicket.jbpm.engine.utils.JbpmEngineUtils;
import org.wicket.jbpm.engine.utils.JbpmVars;
import org.wicket.jbpm.engine.workflow.WfDefinition;
import org.wicket.jbpm.engine.workflow.WfDeployment;
import org.wicket.jbpm.engine.workflow.WfPath;
import org.wicket.jbpm.engine.workflow.WfTask;
import org.wicket.jbpm.engine.workflow.WfTaskState;

public class JbpmProcessEngineImpl extends BPMEngine implements JbpmProcessEngine
{

    private static final Log log = LogFactory.getLog(JbpmProcessEngineImpl.class);

    private final static String WORKFLOW_PATH_SEPERATOR = "-";
    private final static String WORKFLOW_TOKEN_SEPERATOR = "@";

    /**
     * Sets the JBPM Template used for accessing JBoss JBPM in the correct
     * context
     * 
     * @param jbpmTemplate
     */
    protected JbpmTemplate jbpmTemplate;

    public void setJbpmTemplate(JbpmTemplate jbpmTemplate)
    {
	this.jbpmTemplate = jbpmTemplate;
    }

    // Note: jBPM query which is not provided out-of-the-box
    // TODO: Check jBPM 3.2 and get this implemented in jBPM
    private final static String COMPLETED_TASKS_QUERY = "select ti "
	    + "from org.jbpm.taskmgmt.exe.TaskInstance as ti "
	    + "where ti.actorId = :actorId " + "and ti.isOpen = false "
	    + "and ti.end is not null";

    // Note: jBPM query which is not provided out-of-the-box
    // TODO: Check jBPMg future and get this implemented in jBPM
    private final static String PROCESS_TIMERS_QUERY = "select timer "
	    + "from org.jbpm.job.Timer timer "
	    + "where timer.processInstance = :process ";

    /*
     * (non-Javadoc)
     * @see org.wicket.jbpm.engine.util.WfComponent#deployDefinition(java.io.InputStream, java.lang.String)
     */
    public WfDeployment deployDefinition(final InputStream workflowDefinition,
	    final String mimetype) throws WicketJbpmException
    {
	try
	{
	    return (WfDeployment) jbpmTemplate.execute(new JbpmCallback()
	    {
		public Object doInJbpm(JbpmContext context)
		{
		    // construct process definition
		    ProcessDefinition compiledDef = compileProcessDefinition(
			    workflowDefinition, mimetype);

		    // deploy the parsed definition
		    context.deployProcessDefinition(compiledDef);

		    // return deployed definition
		    WfDeployment wfDeployment = createWorkflowDeployment(compiledDef);
		    return wfDeployment;
		}
	    });
	} catch (JbpmException e)
	{
	    throw new WicketJbpmException(
		    "Failed to deploy workflow definition - " + e.getMessage(),
		    e);
	}
    }

    protected ProcessDefinition compileProcessDefinition(
	    InputStream definitionStream, String mimetype)
    {
	// String actualMimetype = (mimetype == null) ? MimetypeMap.MIMETYPE_ZIP
	// : mimetype;
	String actualMimetype = mimetype;
	ProcessDefinition compiledDef = null;
	if (actualMimetype.equals("text/xml"))
	{
	    try
	    {
		compiledDef = ProcessDefinition
			.parseXmlInputStream(definitionStream);
	    } catch (Exception e)
	    {
		throw new JbpmException(
			"Failed to parse process definition from jBPM xml stream",
			e);
	    }
	}

	else
	{
	    throw new JbpmException(
		    "Failed to parse process definition - unsupported mime type '"
			    + mimetype + "'");
	}
	return compiledDef;
    }

    /**
     * Creates a Workflow Deployment
     * 
     * @param compiledDef
     *            compiled JBPM process definition
     * @return workflow deployment
     */
    protected WfDeployment createWorkflowDeployment(
	    ProcessDefinition compiledDef)
    {
	WfDeployment deployment = new WfDeployment();
	// deployment.definition = createWorkflowDefinition(compiledDef);
	// deployment.problems = compiledDef.problems;
	return deployment;
    }

    protected WfPath createWorkflowPath(Token token)
    {
	return JbpmEngineUtils.createWorkflowPath(token);
    }

    /*
     * (non-Javadoc)
     * @see org.wicket.jbpm.engine.util.WfComponent#getAllDefinitions()
     */
    @SuppressWarnings("unchecked")
    public List<WfDefinition> getAllDefinitions()
    {
	try
	{
	    return (List<WfDefinition>) jbpmTemplate.execute(new JbpmCallback()
	    {
		public Object doInJbpm(JbpmContext context)
		{
		    GraphSession graphSession = context.getGraphSession();
		    List<ProcessDefinition> processDefs = (List<ProcessDefinition>) graphSession
			    .findAllProcessDefinitions();
		    return processDefs;
		}
	    });

	} catch (JbpmException e)
	{
	    throw new WicketJbpmException(
		    "Failed to retrieve workflow definitions", e);
	}

    }

    /*
     * (non-Javadoc)
     * @see org.wicket.jbpm.engine.spring.JbpmProcessEngine#getDefinitions()
     */
    @SuppressWarnings("unchecked")
    public List<ProcessDefinition> getDefinitions() throws WicketJbpmException
    {
	try
	{
	    return (List<ProcessDefinition>) jbpmTemplate
		    .execute(new JbpmCallback()
		    {
			public Object doInJbpm(JbpmContext context)
			{
			    GraphSession graphSession = context
				    .getGraphSession();
			    List<ProcessDefinition> processDefs = (List<ProcessDefinition>) graphSession
				    .findLatestProcessDefinitions();

			    return processDefs;
			}
		    });
	} catch (JbpmException e)
	{
	    throw new WicketJbpmException(
		    "Failed to retrieve workflow definitions", e);
	}
    }

    private ProcessDefinition getProcessDefinition(GraphSession graphSession,
	    String workflowDefinitionId)
    {
	ProcessDefinition processDefinition = graphSession
		.getProcessDefinition(getJbpmId(workflowDefinitionId));
	return processDefinition;

    }

    protected ProcessInstance getProcessInstance(GraphSession graphSession,
	    String workflowId)
    {
	ProcessInstance processInstance = graphSession
		.getProcessInstance(getJbpmId(workflowId));
	if (processInstance == null)
	{
	    throw new WicketJbpmException("Workflow instance '" + workflowId
		    + "' does not exist");
	}
	return processInstance;
    }

    /**
     * Get JBoss JBPM Id from Engine Global Id
     * 
     * @param id
     *            global id
     * @return JBoss JBPM Id
     */
    protected long getJbpmId(String id)
    {
	try
	{
	    String theLong = id;
	    return new Long(theLong);
	} catch (NumberFormatException e)
	{
	    throw new WicketJbpmException("Format of id '" + id
		    + "' is invalid", e);
	}
    }

    /*
     * (non-Javadoc)
     * @see org.wicket.jbpm.engine.spring.JbpmProcessEngine#getDefinition(java.lang.String)
     */
    public ProcessDefinition getDefinition(final String name) throws WicketJbpmException
    {
	try
	{
	    return (ProcessDefinition) jbpmTemplate.execute(new JbpmCallback()
	    {
		public Object doInJbpm(JbpmContext context)
		{
		    GraphSession graphSession = context.getGraphSession();
		    ProcessDefinition processDefs = graphSession
			    .findLatestProcessDefinition(name);

		    return processDefs;
		}
	    });
	} catch (JbpmException e)
	{
	    throw new WicketJbpmException(
		    "Failed to retrieve workflow definitions", e);
	}
    }

    /**
     * transform a JBPM Object into Igenko Object
     * 
     * @param processDef
     * @return the preocess definition associtaed to the process definitio
     */
    private WfDefinition getWorkflowDefinition(ProcessDefinition processDef)
    {
	WfDefinition result = null;
	if (processDef != null)
	{
	    result = new WfDefinition(String.valueOf(processDef.getId()),
		    processDef.getName(), String.valueOf(processDef
			    .getVersion()), processDef.getName(), processDef
			    .getDescription());
	}
	return result;

    }
   

    public long getProcessInstanceIdForTaskInstance(final long taskInstanceId)
    {
	return (Long) jbpmTemplate.execute(new JbpmCallback()
	{
	    public Object doInJbpm(JbpmContext jbpmContext)
		    throws JbpmException
	    {
		TaskInstance ti = getTaskInstance(taskInstanceId);
		ProcessInstance pi = ti.getToken().getProcessInstance();
		return pi.getId();
	    }
	});
    }

    protected TaskInstance getTaskInstance(final long tid)
    {
	return (TaskInstance) jbpmTemplate.execute(new JbpmCallback()
	{
	    public Object doInJbpm(JbpmContext jbpmContext)
		    throws JbpmException
	    {
		try
		{
		    return _taskInstance(jbpmContext.getSession(), jbpmContext,
			    tid);
		} catch (Throwable throwable)
		{
		    // getLoggingUtils().log(throwable);
		}
		return null;
	    }
	});
    }

    private TaskInstance _taskInstance(Session session, JbpmContext ctx,
	    long taskInstanceId) throws Throwable
    {
	TaskInstance taskInstance = null;
	try
	{
	    taskInstance = (TaskInstance) session.load(TaskInstance.class,
		    taskInstanceId, LockMode.UPGRADE);
	} catch (Throwable e)
	{
	    // getLoggingUtils().log(e);
	    throw new JbpmException("couldn't get task instance '"
		    + taskInstanceId + "'", e);
	}
	return taskInstance;
    }

    public String getProcInstKey(final long processInstanceId)
    {
	try
	{
	    return (String) jbpmTemplate.execute(new JbpmCallback()
	    {
		public Object doInJbpm(JbpmContext context)
		{
		    // construct new process
		    ProcessInstance processInstance = context.getProcessInstance(processInstanceId);
		    return processInstance.getKey();
		}
	    });
	} catch (JbpmException e)
	{
	    throw new WicketJbpmException(
		    "Failed to retrieve process instance", e);
	}
    }

    protected TaskInstance getTaskInstance(TaskMgmtSession taskSession,
	    String taskId)
    {
	TaskInstance taskInstance = taskSession
		.getTaskInstance(getJbpmId(taskId));

	return taskInstance;
    }

    /**
     * Helper to retrieve the real jBPM Node
     * 
     * @param node
     *            Node
     * @return real Node (i.e. the one that's not a Hibernate proxy)
     */
    /*
     * private Node getRealNode(Node node) { if (node instanceof HibernateProxy)
     * { Node realNode =
     * (Node)((HibernateProxy)node).getHibernateLazyInitializer
     * ().getImplementation(); return realNode; } else { return node; } }
     */

    /**
     * Get the Workflow Task State for the specified JBoss JBPM Task
     * 
     * @param task
     *            task
     * @return task state
     */
    protected WfTaskState getWorkflowTaskState(TaskInstance task)
    {
	if (task.hasEnded())
	{
	    return WfTaskState.COMPLETED;
	} else
	{
	    return WfTaskState.IN_PROGRESS;
	}
    }

    /**
     * create a map representing the task variable
     * 
     * @param task
     * @return the map representing the task variables
     */
    @SuppressWarnings("unchecked")
    private Map<String, Serializable> getTaskVariables(TaskInstance task)
    {
	Map<String, Serializable> taskVariables = new HashMap<String, Serializable>();
	Map<String, Serializable> vars = task.getVariables();
	Set varSet = vars.entrySet();
	for (Iterator iter = varSet.iterator(); iter.hasNext();)
	{
	    Entry<String, Object> var = (Entry<String, Object>) iter.next();
	    String name = var.getKey();
	    Object value = var.getValue();
	    taskVariables.put(name, (Serializable) value);

	}
	return taskVariables;
    }

    /**
     * Get the JBoss JBPM Token for the Workflow Path
     * 
     * @param session
     *            JBoss JBPM Graph Session
     * @param pathId
     *            workflow path id
     * @return JBoss JBPM Token
     */
    protected Token getWorkflowToken(GraphSession session, String pathId)
    {
	// extract process id and token path within process
	String[] path = pathId.split(WORKFLOW_PATH_SEPERATOR);
	if (path.length != 2)
	{
	    throw new WicketJbpmException("Invalid workflow path '" + pathId
		    + "'");
	}

	// retrieve jBPM token for workflow position
	ProcessInstance processInstance = getProcessInstance(session, path[0]);
	String tokenId = path[1].replace(WORKFLOW_TOKEN_SEPERATOR, "/");
	Token token = processInstance.findToken(tokenId);
	if (token == null)
	{
	    throw new WicketJbpmException("Workflow path '" + pathId
		    + "' does not exist");
	}

	return token;
    }

    public WfTask updateTask(WfTask task)
    {
	final String taskInstanceId = task.id;
	final WfTask wfTask = task;
	try
	{
	    return (WfTask) jbpmTemplate.execute(new JbpmCallback()
	    {
		public Object doInJbpm(JbpmContext context)
		{
		    // retrieve task
		    TaskMgmtSession taskSession = context.getTaskMgmtSession();
		    TaskInstance taskInstance = getTaskInstance(taskSession,
			    taskInstanceId);

		    // update JBPM Object : variables only
		    log.debug(" variables during update :" + wfTask.variables);
		    Set<Entry<String, Serializable>> varSet = wfTask.variables
			    .entrySet();
		    for (Iterator<Entry<String, Serializable>> iter = varSet
			    .iterator(); iter.hasNext();)
		    {
			Entry<String, Serializable> var = iter.next();
			String name = var.getKey();
			Object value = var.getValue();
			taskInstance.setVariable(name, value);

		    }

		    // save
		    context.save(taskInstance.getProcessInstance());

		    return JbpmEngineUtils.createWorkflowTask(engineId,getTaskInstance(taskSession,taskInstanceId));
		}
	    });
	} catch (Exception e)
	{
	    throw new WicketJbpmException("Failed to update workflow task '"
		    + task.id + "' : " + e.getMessage(), e);
	}
    }

    public String[] getProcessName(final Long taskInstanceId) throws WicketJbpmException
    {
	try
	{
	    return (String[]) jbpmTemplate.execute(new JbpmCallback()
	    {
		public Object doInJbpm(JbpmContext context)
			throws JbpmException
		{
		    String defs[] = new String[2];
		    TaskInstance ti = getTaskInstance(taskInstanceId);
		    ProcessInstance pi = ti.getToken().getProcessInstance();
		    ProcessDefinition def = pi.getProcessDefinition();
		    
		    defs[0] = def.getName();
		    defs[1] = def.getDescription();
		    
		    return defs;
		}
	    });
	} catch (Exception ex)
	{
	    throw new WicketJbpmException("Error getting process name for task:"+taskInstanceId,ex);
	}
    }

	public Stack<JbpmVars> getJbpmVars(WfTask wfTask) 
	{
		
		final Map<String, Serializable> taskVars = wfTask.variables;
		final Stack<JbpmVars> taskvars = new Stack<JbpmVars>();
		
		final Set<Entry<String,Serializable>> varSet = taskVars.entrySet();
		for (Iterator<Entry<String,Serializable>> iter = varSet.iterator(); iter.hasNext();)
		{
		    Entry<String, Serializable> var =  iter.next();
		    String name = var.getKey();
		    Object value = var.getValue();
		   
		    taskvars.push(new JbpmVars(name,value));
		}
		
		return taskvars;
	}

   
}
